package com.google.code.mergeinformer.util.synchronization;

import java.util.Map;
import java.util.WeakHashMap;

/**
 * @author Dmitry Golubev
 */
public class MutexProvider<K, O> {

    private final Map<Mutex<K, O>, Mutex<K, O>> mutexes = new WeakHashMap<>();

    public MutexProvider() {
    }

    /**
     * Return mutex (monitor for synchronized block) for particular object.
     * <p/>
     * It is guaranteed that the same monitor (same reference) is returned for the same object (same in case of .equals).
     * <p/>
     * To get different monitors for one object (e.g. to synchronize independent operations), <code>operations</code> parameter is used.
     * @param key
     * @param operations
     * @return
     */
    @SafeVarargs
    public final Mutex getMutex(K key, O... operations) {
        return getMutexInternal(new Mutex<>(key, operations));
    }

    /**
     * Try to execute block of code extrusive, i.e. if no other threads are executing this block of code (locked by
     * <code>key</code> and <code>operations</code> mutex)? current thread will execute it, otherwise it will wait until
     * concurrent thread will complete execution and then exit without entering this block.
     * <p/>
     * <b>Works only if concurrent threads locking via invocation of this method!</b> Otherwise concurrent threads can be blocked,
     * but not extruded: this block of code will be run sequentially.
     * @param extrusiveBlock
     * @param key
     * @param operations
     */
    @SafeVarargs
    public final void executeExtrusive(final ExtrusiveBlock extrusiveBlock, final K key, final O... operations) {
        final ExtrusiveMutex extrusiveMutex = (ExtrusiveMutex) getMutexInternal(new ExtrusiveMutex<>(this, key, operations));
        extrusiveMutex.executeExtrusiveBlock(new ExtrusiveBlock() {
            @Override
            public void execute() {
                // get common mutex that will block (not extrude) operations that are locked by common mutex
                synchronized (getMutexInternal(new Mutex<>(key, operations))) {
                    extrusiveBlock.execute();
                }
            }
        });
    }

    private synchronized Mutex getMutexInternal(Mutex<K, O> mutex) {
        final Mutex<K, O> existingMutex = mutexes.get(mutex);
        if (existingMutex != null) {
            return existingMutex;
        }
        mutexes.put(mutex, mutex);
        return mutex;
    }

    synchronized void discardMutex(Mutex<K, O> mutex) {
        mutexes.remove(mutex);
    }

    public static interface ExtrusiveBlock {
        void execute();
    }
}